﻿namespace System.Numerics.Geometry
{
	using System.Diagnostics;
	using System.Globalization;
	using System.Runtime.InteropServices;

	/// <summary>
	/// Representation of a bounding sphere.
	/// </summary>
	[Serializable]
	[DebuggerDisplay("Position = ({Position}) Radius = {Radius}")]
	[StructLayout(LayoutKind.Sequential, Pack = 1)]
	public struct BoundingSphere
	{
		#region Constructor
		/// <summary>
		/// Initializes a new instance of the struct.
		/// </summary>
		/// <param name="position">The center point.</param>
		/// <param name="radius">The bounding radius.</param>
		public BoundingSphere(Vector3 position, Single radius)
		{
			this.Position = position;
			this.Radius = radius;
		}
		#endregion

		#region Methods
		/// <summary>
		/// Determines whether the current object is equal to the specified object.
		/// </summary>
		/// <param name="value">An object.</param>
		/// <returns>True if the current object is equal to the <paramref name="value"/>, otherwise false.</returns>
		public override bool Equals(object value)
		{
			if (value is BoundingSphere)
			{
				return Equals((BoundingSphere)value);
			}

			return false;
		}

		/// <summary>
		/// Determines whether the current bounding sphere is equal to the specified bounding sphere.
		/// </summary>
		/// <param name="value">A bounding sphere.</param>
		/// <returns>True if the current bounding sphere is equal to the <paramref name="value"/>, otherwise false.</returns>
		public bool Equals(BoundingSphere value)
		{
			return
				this.Position == value.Position &&
				this.Radius == value.Radius;
		}

		/// <summary>
		/// Returns a hash code for the current bounding sphere.
		/// </summary>
		/// <returns>A hash code.</returns>
		/// <remarks>
		/// The hash code is not unique.
		/// If two bounding spheres are equal, their hash codes are guaranteed to be equal.
		/// If the bounding spheres are not equal, their hash codes are not guaranteed to be different.
		/// </remarks>
		public override int GetHashCode()
		{
			return
				this.Position.GetHashCode() ^
				this.Radius.GetHashCode();
		}

		/// <summary>
		/// Determines whether any of the fields of the specified bounding sphere evaluates to an infinity.
		/// </summary>
		/// <param name="value">A bounding sphere.</param>
		/// <returns>True if any of the fields of <paramref name="value"/> evaluates to an infinity, otherwise false.</returns>
		public static bool IsInfinity(BoundingSphere value)
		{
			return
				Vector3.IsInfinity(value.Position) ||
				Single.IsInfinity(value.Radius);
		}

		/// <summary>
		/// Determines whether any of the fields of the specified bounding sphere evaluates to a value that is not a number.
		/// </summary>
		/// <param name="value">A bounding sphere.</param>
		/// <returns>True if any of the fields of <paramref name="value"/> evaluates to a value that is not a number, otherwise false.</returns>
		public static bool IsNaN(BoundingSphere value)
		{
			return
				Vector3.IsNaN(value.Position) ||
				Single.IsNaN(value.Radius);
		}

		/// <summary>
		/// Determines whether any of the fields of the specified bounding sphere evaluates to negative infinity.
		/// </summary>
		/// <param name="value">A bounding sphere.</param>
		/// <returns>True if any of the fields of <paramref name="value"/> evaluates to negative infinity, otherwise false.</returns>
		public static bool IsNegativeInfinity(BoundingSphere value)
		{
			return
				Vector3.IsNegativeInfinity(value.Position) ||
				Single.IsNegativeInfinity(value.Radius);
		}

		/// <summary>
		/// Determines whether any of the fields of the specified bounding sphere evaluates to positive infinity.
		/// </summary>
		/// <param name="value">A bounding sphere.</param>
		/// <returns>True if any of the fields of <paramref name="value"/> evaluates to positive infinity, otherwise false.</returns>
		public static bool IsPositiveInfinity(BoundingSphere value)
		{
			return
				Vector3.IsPositiveInfinity(value.Position) ||
				Single.IsPositiveInfinity(value.Radius);
		}

		/// <summary>
		/// Returns the smallest bounding sphere containing both of the specified bounding spheres.
		/// </summary>
		/// <param name="value1">A bouding sphere.</param>
		/// <param name="value2">A bouding sphere.</param>
		/// <returns>The smallest bounding sphere containing both of the <paramref name="value1"/> and the <paramref name="value2"/>.</returns>
		public static BoundingSphere Merge(BoundingSphere value1, BoundingSphere value2)
		{
			var separation = value2.Position - value1.Position;
			var distance = separation.Magnitude;

			if (value1.Radius - value2.Radius >= distance)
			{
				return value1;
			}
			else if (value2.Radius - value1.Radius >= distance)
			{
				return value2;
			}
			else
			{
				var radius = (distance + value1.Radius + value2.Radius) * 0.5f;
				var d = (radius - value1.Radius) / distance;

				return new BoundingSphere
				{
					Position = value1.Position + separation * d,
					Radius = radius,
				};
			}
		}

		/// <summary>
		/// Returns the bounding sphere parsed from the specified string.
		/// </summary>
		/// <param name="value">The string to parse.</param>
		/// <returns>The bounding sphere parsed from the <paramref name="value"/>.</returns>
		public static BoundingSphere Parse(string value)
		{
			return Parse(value, NumberStyles.Float | NumberStyles.AllowThousands, null);
		}

		/// <summary>
		/// Returns the bounding sphere parsed from the specified string.
		/// </summary>
		/// <param name="value">The string to parse.</param>
		/// <param name="numberStyle">The number style for each component.</param>
		/// <returns>The bounding sphere parsed from the <paramref name="value"/>.</returns>
		public static BoundingSphere Parse(string value, NumberStyles numberStyle)
		{
			return Parse(value, numberStyle, null);
		}

		/// <summary>
		/// Returns the bounding sphere parsed from the specified string.
		/// </summary>
		/// <param name="value">The string to parse.</param>
		/// <param name="formatProvider">The format provider for each component.</param>
		/// <returns>The bounding sphere parsed from the <paramref name="value"/>.</returns>
		public static BoundingSphere Parse(string value, IFormatProvider formatProvider)
		{
			return Parse(value, NumberStyles.Float | NumberStyles.AllowThousands, formatProvider);
		}

		/// <summary>
		/// Returns the bounding sphere parsed from the specified string.
		/// </summary>
		/// <param name="value">The string to parse.</param>
		/// <param name="numberStyle">The number style for each component.</param>
		/// <param name="formatProvider">The format provider for each component.</param>
		/// <returns>The bounding sphere parsed from the <paramref name="value"/>.</returns>
		public static BoundingSphere Parse(string value, NumberStyles numberStyle, IFormatProvider formatProvider)
		{
			if (value == null)
			{
				throw new ArgumentNullException("value");
			}

			var input = value.Split(Internals.Whitespace, StringSplitOptions.RemoveEmptyEntries);

			if (input.Length == ValueCount)
			{
				BoundingSphere result;
				result.Position.X = Single.Parse(input[0], numberStyle, formatProvider);
				result.Position.Y = Single.Parse(input[1], numberStyle, formatProvider);
				result.Position.Z = Single.Parse(input[2], numberStyle, formatProvider);
				result.Radius = Single.Parse(input[3], numberStyle, formatProvider);
				return result;
			}
			else
			{
				throw new FormatException("Invalid number of components in the source string.");
			}
		}

		/// <summary>
		/// Returns a string representation of the current object.
		/// </summary>
		/// <returns>A string representation of the current object.</returns>
		public override string ToString()
		{
			return ToString(null, null);
		}

		/// <summary>
		/// Returns a string representation of the current object.
		/// </summary>
		/// <param name="format">Format string.</param>
		/// <returns>A string representation of the current object.</returns>
		public string ToString(string format)
		{
			return ToString(format, null);
		}

		/// <summary>
		/// Returns a string representation of the current object.
		/// </summary>
		/// <param name="formatProvider">Format provider.</param>
		/// <returns>A string representation of the current object.</returns>
		public string ToString(IFormatProvider formatProvider)
		{
			return ToString(null, formatProvider);
		}

		/// <summary>
		/// Returns a string representation of the current object.
		/// </summary>
		/// <param name="format">Format string.</param>
		/// <param name="formatProvider">Format provider.</param>
		/// <returns>A string representation of the current object.</returns>
		public string ToString(string format, IFormatProvider formatProvider)
		{
			return Internals.Format(format, formatProvider, this.Position.X, this.Position.Y, this.Position.Z, this.Radius);
		}

		/// <summary>
		/// Attempts to parse the bounding sphere from the specified string.
		/// </summary>
		/// <param name="value">The string to parse.</param>
		/// <param name="result">The output variable for the bounding sphere parsed from the <paramref name="value"/>.</param>
		/// <returns>True if the <paramref name="value"/> was parsed successfully, otherwise false.</returns>
		public static bool TryParse(string value, out BoundingSphere result)
		{
			return TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, null, out result);
		}

		/// <summary>
		/// Attempts to parse the bounding sphere from the specified string.
		/// </summary>
		/// <param name="value">The string to parse.</param>
		/// <param name="numberStyle">The number style for each component.</param>
		/// <param name="result">The output variable for the bounding sphere parsed from the <paramref name="value"/>.</param>
		/// <returns>True if the <paramref name="value"/> was parsed successfully, otherwise false.</returns>
		public static bool TryParse(string value, NumberStyles numberStyle, out BoundingSphere result)
		{
			return TryParse(value, numberStyle, null, out result);
		}

		/// <summary>
		/// Attempts to parse the bounding sphere from the specified string.
		/// </summary>
		/// <param name="value">The string to parse.</param>
		/// <param name="formatProvider">The format provider for each component.</param>
		/// <param name="result">The output variable for the bounding sphere parsed from the <paramref name="value"/>.</param>
		/// <returns>True if the <paramref name="value"/> was parsed successfully, otherwise false.</returns>
		public static bool TryParse(string value, IFormatProvider formatProvider, out BoundingSphere result)
		{
			return TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, formatProvider, out result);
		}

		/// <summary>
		/// Attempts to parse the bounding sphere from the specified string.
		/// </summary>
		/// <param name="value">The string to parse.</param>
		/// <param name="numberStyle">The number style for each component.</param>
		/// <param name="formatProvider">The format provider for each component.</param>
		/// <param name="result">The output variable for the bounding sphere parsed from the <paramref name="value"/>.</param>
		/// <returns>True if the <paramref name="value"/> was parsed successfully, otherwise false.</returns>
		public static bool TryParse(string value, NumberStyles numberStyle, IFormatProvider formatProvider, out BoundingSphere result)
		{
			if (value == null)
			{
				throw new ArgumentNullException("value");
			}

			result = new BoundingSphere();

			var input = value.Split(Internals.Whitespace, StringSplitOptions.RemoveEmptyEntries);

			return
				input.Length == ValueCount &&
				Single.TryParse(input[0], numberStyle, formatProvider, out result.Position.X) &&
				Single.TryParse(input[1], numberStyle, formatProvider, out result.Position.Y) &&
				Single.TryParse(input[2], numberStyle, formatProvider, out result.Position.Z) &&
				Single.TryParse(input[3], numberStyle, formatProvider, out result.Radius);
		}

		/// <summary>
		/// Returns the current bounding sphere with the specified fields replaced with the specified values.
		/// </summary>
		/// <param name="position">The new value for the <see cref="Position"/> field. Use null to keep the old value.</param>
		/// <param name="radius">The new value for the <see cref="Radius"/> field. Use null to keep the old value.</param>
		/// <returns>The current bounding sphere with the specified fields replaced with the specified values.</returns>
		public BoundingSphere With(Vector3? position = null, Single? radius = null)
		{
			return new BoundingSphere
			{
				Position = position ?? this.Position,
				Radius = radius ?? this.Radius,
			};
		}
		#endregion

		#region Operators
		/// <summary>
		/// Determines whether the bounding spheres are equal.
		/// </summary>
		/// <param name="value1">A bounding sphere.</param>
		/// <param name="value2">A bounding sphere.</param>
		/// <returns>True if the bounding spheres are equal, otherwise false.</returns>
		public static bool operator ==(BoundingSphere value1, BoundingSphere value2)
		{
			return
				value1.Position == value2.Position &&
				value1.Radius == value2.Radius;
		}

		/// <summary>
		/// Determines whether the bounding spheres are not equal.
		/// </summary>
		/// <param name="value1">A bounding sphere.</param>
		/// <param name="value2">A bounding sphere.</param>
		/// <returns>True if the bounding spheres are not equal, otherwise false.</returns>
		public static bool operator !=(BoundingSphere value1, BoundingSphere value2)
		{
			return
				value1.Position != value2.Position ||
				value1.Radius != value2.Radius;
		}
		#endregion

		#region Properties
		/// <summary>
		/// Gets a bounding sphere with the fields set to a value that is not a number.
		/// </summary>
		/// <value>The bounding sphere with the fields set to a value that is not a number.</value>
		public static BoundingSphere NaN
		{
			get { return nan; }
		}

		/// <summary>
		/// Gets a bounding sphere with the fields set to negative infinity.
		/// </summary>
		/// <value>The bounding sphere with the fields set to negative infinity.</value>
		public static BoundingSphere NegativeInfinity
		{
			get { return negativeInfinity; }
		}

		/// <summary>
		/// Gets a bounding sphere with the fields set to positive infinity.
		/// </summary>
		/// <value>The bounding sphere with the fields set to positive infinity.</value>
		public static BoundingSphere PositiveInfinity
		{
			get { return positiveInfinity; }
		}

		/// <summary>
		/// Gets a bounding sphere with the fields set to zero.
		/// </summary>
		/// <value>The bounding sphere with the fields set to zero.</value>
		public static BoundingSphere Zero
		{
			get { return zero; }
		}
		#endregion

		#region Fields
		/// <summary>
		/// Center point.
		/// </summary>
		public Vector3 Position;

		/// <summary>
		/// Bounding radius.
		/// </summary>
		public Single Radius;

		/// <summary>
		/// Number of values in the structure.
		/// </summary>
		private const int ValueCount = 4;

		/// <summary>
		/// A bounding sphere with the fields set to a value that is not a number.
		/// </summary>
		private static readonly BoundingSphere nan = new BoundingSphere(Vector3.NaN, Single.NaN);

		/// <summary>
		/// A bounding sphere with the fields set to negative infinity.
		/// </summary>
		private static readonly BoundingSphere negativeInfinity = new BoundingSphere(Vector3.NegativeInfinity, Single.NegativeInfinity);

		/// <summary>
		/// A bounding sphere with the fields set to positive infinity.
		/// </summary>
		private static readonly BoundingSphere positiveInfinity = new BoundingSphere(Vector3.PositiveInfinity, Single.PositiveInfinity);

		/// <summary>
		/// A bounding sphere with the fields set to zero.
		/// </summary>
		private static readonly BoundingSphere zero = new BoundingSphere(Vector3.Zero, 0);
		#endregion
	}
}
